using System;
using System.Collections.Generic;

namespace ZFramework.Voxel
{
    public interface IVoxelMap<V>
    {
        bool HasVoxel(int x, int y, int z);
        bool HasVoxel(Vec3I voxelPos) => HasVoxel(voxelPos.x, voxelPos.y, voxelPos.z);
        V VoxelAt(int x, int y, int z);

        bool Includes(Vec3 position) => HasVoxel(position.FloorToInt());
        

        VoxelHitResult? Intersects(Ray ray, float maxDistance)
        {
            var originVoxelPos = ray.Origin.FloorToInt();
            if (HasVoxel(originVoxelPos))
            {
                return new VoxelHitResult
                {
                    voxelPos = originVoxelPos,
                    normal = null,
                };
            }

            // Sort axis by absolute value.
            Span<int> axis = stackalloc int[3];
            for (var i = 1; i < 3; i++)
            {
                if (MathF.Abs(ray.Direction[i]) > MathF.Abs(ray.Direction[axis[0]]))
                {
                    axis[0] = i;
                }
            }
            axis[1] = (axis[0] + 1) % 3;
            axis[2] = (axis[0] + 2) % 3;
            if (MathF.Abs(ray.Direction[axis[1]]) < MathF.Abs(ray.Direction[axis[2]]))
            {
                (axis[1], axis[2]) = (axis[2], axis[1]);
            }


            Span<float> signDir = stackalloc float[3];
            for (int i = 0; i < axis.Length; i++)
            {
                signDir[i] = MathF.Sign(ray.Direction[axis[i]]);
            }

            var prevPos = ray.Origin;

            Span<float> plane = stackalloc float[3];
            plane[0] = ray.Direction[axis[0]] > 0 ? MathF.Ceiling(ray.Origin[axis[0]]) : originVoxelPos[axis[0]];

            Span<float> t = stackalloc float[3];
            t[0] = (plane[0] - ray.Origin[axis[0]]) / ray.Direction[axis[0]];

            while (t[0] <= maxDistance)
            {
                var curPos = new Vec3();
                curPos[axis[0]] = plane[0];
                curPos[axis[1]] = ray.Origin[axis[1]] + t[0] * ray.Direction[axis[1]];
                curPos[axis[2]] = ray.Origin[axis[2]] + t[0] * ray.Direction[axis[2]];

                var prevVoxelPos = prevPos.FloorToInt();
                var curVoxelPos = curPos.FloorToInt();

                // Check other axis first.
                for (int i = 1; i < 3; i++)
                {
                    if (prevVoxelPos[axis[i]] != curVoxelPos[axis[i]])
                    {
                        plane[i] = signDir[i] > 0 ? curVoxelPos[axis[i]] : prevVoxelPos[axis[i]];
                        t[i] = (plane[i] - ray.Origin[axis[i]]) / ray.Direction[axis[i]];

                        var extraPos = new Vec3();
                        extraPos[axis[i]] = plane[i];
                        extraPos[axis[(i + 1) % 3]] = ray.Origin[axis[(i + 1) % 3]] + t[i] * ray.Direction[axis[(i + 1) % 3]];
                        extraPos[axis[(i + 2) % 3]] = ray.Origin[axis[(i + 2) % 3]] + t[i] * ray.Direction[axis[(i + 2) % 3]];

                        var extraVoxelPos = extraPos.FloorToInt();

                        if (ray.Origin[axis[i]] > plane[i])
                        {
                            extraVoxelPos[axis[i]] -= 1;
                        }

                        if (HasVoxel(extraVoxelPos))
                        {
                            var normal = Vec3.Zero;
                            normal[axis[i]] = -signDir[i];

                            return new VoxelHitResult
                            {
                                voxelPos = extraVoxelPos,
                                normal = normal,
                            };
                        }
                    }
                }

                if (ray.Origin[axis[0]] > plane[0])
                {
                    curVoxelPos[axis[0]] -= 1;
                }

                if (HasVoxel(curVoxelPos))
                {
                    var normalMain = Vec3.Zero;
                    normalMain[axis[0]] = -signDir[0];

                    return new VoxelHitResult
                    {
                        voxelPos = curVoxelPos,
                        normal = normalMain,
                    };
                }

                prevPos = curPos;

                plane[0] += signDir[0];
                t[0] = (plane[0] - ray.Origin[axis[0]]) / ray.Direction[axis[0]];
            }
            return null;
        }

        private static char GetAxis(int idx) => idx switch
        {
            0 => 'x',
            1 => 'y',
            2 => 'z',
            _ => throw new IndexOutOfRangeException(),
        };
    }
}